﻿import sqlite3 as sqlite

class PTable(object):
    """
    Класс-обёртка над таблицей из SQLite.
    """

    def __init__(self, name: str, connection):
        """
        Создаёт таблицу с указанным именем и соединением с БД.
        """
        # Соединение с БД.
        self._connection = connection

        # Имя таблицы.
        self.name = name

        # Имена столбцов, являющиеся первичными ключами.
        self.primaryKeys = []

        # Имена вторичные ключи (внешние), по умолчанию отсутствуют, заданные следующим образом:
        # [(имя_столбца_внешний_ключ, имя_внешней_таблици, имя_столбца_первичный_ключ_внешней_таблици),]
        self.foreignKeys = []

        # Столбцы таблицы представляет собой список, следующего вида:
        # [(номер_столбца, имя_столбца, тип_столбца),].
        self.columns = []

        # Получение структуры таблицы.
        try:
            cursor = self._connection.cursor() # Получение курсора соединения.
            # Полчучение информации о столбцах и первичных ключах данной таблицы.
            cursor.execute('PRAGMA table_info(`{}`)'.format(self.name))
            for col in cursor.fetchall():
                self.columns.append((col[0], col[1], col[2] if col[2].find('VARCHAR') == -1 else 'VARCHAR'))
                if col[5] == 1:
                    self.primaryKeys.append(col[1])
            # Полчучение информации о вторичных ключах данной таблицы.
            cursor.execute('PRAGMA foreign_key_list(`{}`)'.format(self.name))
            for col in cursor.fetchall():
                self.foreignKeys.append((col[3], col[2], col[4]))
        except sqlite.Error as ex:
            raise ex

    def fetchColumnsWithoutKeys(self):
        """
        Получение всех столбцов, не являющихся первичными и вторичными, с указанием имен, номеров и типов.
        # список кортежей вида - [(номер_столбца, имя_столбца, тип_столбца),]
        """
        keys = set(self.primaryKeys) | set(fk[0] for fk in self.foreignKeys)
        return [col for col in self.columns if not col[1] in keys]

    def fetchColumnsWithoutForeignKeys(self):
        """
        Получение всех столбцов, не являющихся вторичными, с указанием имен, номеров и типов.
        # список кортежей вида - [(номер_столбца, имя_столбца, тип_столбца),]
        """
        keys = set(fk[0] for fk in self.foreignKeys)
        return [col for col in self.columns if not col[1] in keys]

    def addRow(self, values):
        """
        Добаление в таблицу строки с указанными значениями.
        Возращает id добавленной строки.
        """
        if len(values) < len(self.columns) - 1:
            raise sqlite.Error("Невозможно добавить строку: количество столбцов добавляемой строки не соответвует количеству столбцов в таблице.")

        # В случае если это словарь.
        if type(values) == dict:
            colNames = [c[1] for c in self.columns]
            for v in values:
                if v in colNames:
                    continue
                else:
                    raise sqlite.Error("Невозможно добавить строку, так как столбцы добавляемой строки не соответвуют столбцам таблицы.")
            query = "INSERT INTO `{}`({}) VALUES({})".format(
                self.name,
                ", ".join('`' + k + '`' for k in values),
                ", ".join('?' for k in values)
            )
            data = tuple([values[k] for k in values])
        # Иначе считаем кортежем или списком, заполнение значений столбцов строки идет слево на право последовательности.
        else:
            query = "INSERT INTO `{}` VALUES({})".format(
                self.name,
                ", ".join('?' for k in values)
            )
            data = tuple(values)
        try:
            cursor = self._connection.cursor() # Получение курсора.
            cursor.execute(query, data)
            self._connection.commit()          # Подтверждение изменений.
            return  cursor.lastrowid           # Получение id вставленной строки.
        except sqlite.Error as ex:
            raise ex

    def clear(self):
        """
        Удаление всех строк таблицы(очистка).
        """
        try:
            cursor = self._connection.cursor() # Получение курсора.
            cursor.execute("DELETE FROM `{}`".format(self.name))
            self._connection.commit()          # Подтверждение изменений.
        except sqlite.Error as ex:
            raise ex

    def deleteRow(self, identifier: int):
        """
        Удаление строки в таблице по указанному id.
        """
        try:
            cursor = self._connection.cursor() # Получение курсора.
            cursor.execute("DELETE FROM `{}` WHERE id = {}".format(self.name, identifier))
            self._connection.commit()          # Подтверждение изменений.
        except sqlite.Error as ex:
            raise ex

    def getAllRows(self):
        """
        Получение всех строк данной таблицы в виде списка кортежей.
        """
        try:
            cursor = self._connection.cursor() # Получение курсора.
            cursor.execute("SELECT * FROM `{}`".format(self.name))
            return cursor.fetchall()
        except sqlite.Error as ex:
            raise ex

    def changeRow(self, identifier: int, values: dict):
        """
        Изменение значений в указанной строке таблицы.
        """
        # Проверка ключей словаря на наличие несуществующих имён столбцов.
        if set(values.keys()).issubset(set([c[1] for c in self.columns])):
            query = "UPDATE `{}` SET {} WHERE id = {}".format(
                    self.name,
                    ", ".join('`' + k + '` = ?'  for k in values),
                    identifier
                )
            cursor = self._connection.cursor() # Получение курсора.
            cursor.execute(query, tuple(values.values()))
            self._connection.commit()          # Подтверждение изменений.
        else:
            raise sqlite.Error("Обнаружено несуществующее название столбца.")

    def select(self, condtions={}, selectTo=[], orderBy=[], desc=False):
        """
        Простая выборка из таблицы, согласно заданным условиям.
        # condtions (dict) - словарь с условиями, где условия разделенны оператором AND.
            key - имя столбца для поиска в таблице,
            value - ограничение. Value может быть либо простым типом данных (для проверки на равенство)
            либо кортежем, где (None, v) - операция '>', (v, None) - операция '<' и (v1, v2) - операция between.
        # selecTo (list, tuple) - поля таблицы оторые будут отобраны, значение по умолчанию - выборка по всем полям.
        # orderBy (list, tuple) - поля таблицы относительно которых будут отсортированны выбранные строки, значение по умолчанию - без упорядочивания.
        return - список найденных строк таблицы, удовлетворяющих заданным условиям.
        """
        arguments = [] # Аргументы запроса
        condition = '' # Полное условие, следующее после оператора SQL 'where'.

        # Составление условий и получение аргументов.
        if len(condtions) != 0:
            for i, key in enumerate(condtions):
                # Проверка на операцию SQL 'between'.
                if type(condtions[key]) == tuple and (condtions[key][0] is not None and condtions[key][1] is not None):
                    condition += ' `' + key + '` BETWEEN ? AND ?'
                    arguments.append(condtions[key][0])
                    arguments.append(condtions[key][1])
                # Проверка на операцию SQL '<'.
                elif type(condtions[key]) == tuple and condtions[key][0] is not None:
                    condition += ' `' + key + '` < ?'
                    arguments.append(condtions[key][0])
                # Проверка на операцию SQL 'between'.
                elif type(condtions[key]) == tuple and condtions[key][1] is not None:
                    condition += ' `' + key + '` > ?'
                    arguments.append(condtions[key][1])
                # Иначе операция SQL '='.
                else:
                    condition += ' `' + key + '` = ?'
                    arguments.append(condtions[key])
                # Соединение операций логическим операндом.
                if i < len(condtions) - 1:
                    condition += ' AND'
        try:
            # Составление запроса.
            query = ('SELECT ' + (', '.join(['`' + s +'`' for s in selectTo]) if len(selectTo) != 0 else '*') + ' FROM `' +
                     self.name + '`' + ((' WHERE ' + condition) if len(condition) != 0 else '') +
                     ((' ORDER BY ' + ', '.join(['`' + o + '`' for o in orderBy]) + 'DESC' if desc else '') if len(orderBy) != 0 else '')
            )
            cursor = self._connection.cursor()  # Получение курсора.
            cursor.execute(query, arguments)
            return cursor.fetchall()
        except Exception as ex:
            raise ex